<?php
/**
 * CCodeModel class file.
 *
 * @author Qiang Xue <qiang.xue@gmail.com>
 * @link http://www.yiiframework.com/
 * @copyright Copyright &copy; 2008-2011 Yii Software LLC
 * @license http://www.yiiframework.com/license/
 */

/**
 * CCodeModel is the base class for model classes that are used to generate code.
 *
 * Each code generator should have at least one code model class that extends from this class.
 * The purpose of a code model is to represent user-supplied parameters and use them to
 * generate customized code.
 *
 * Derived classes should implement the {@link prepare} method whose main task is to
 * fill up the {@link files} property based on the user parameters.
 *
 * The {@link files} property should be filled with a set of {@link CCodeFile} instances,
 * each representing a single code file to be generated.
 *
 * CCodeModel implements the feature of "sticky attributes". A sticky attribute is an attribute
 * that can remember its last valid value, even if the user closes his browser window
 * and reopen it. To declare an attribute is sticky, simply list it in a validation rule with
 * the validator name being "sticky".
 *
 * @author Qiang Xue <qiang.xue@gmail.com>
 * @version $Id: CCodeModel.php 2799 2011-01-01 19:31:13Z qiang.xue $
 * @package system.gii
 * @since 1.1.2
 */
abstract class CCodeModel extends CFormModel
{
    const STATUS_NEW = 1;
    const STATUS_PREVIEW = 2;
    const STATUS_SUCCESS = 3;
    const STATUS_ERROR = 4;
    
    /**
     * @var array user confirmations on whether to overwrite existing code files with the newly generated ones.
     * The value of this property is internally managed by this class and {@link CCodeGenerator}.
     */
    public $answers;
    /**
     * @var string the name of the code template that the user has selected.
     * The value of this property is internally managed by this class and {@link CCodeGenerator}.
     */
    public $template;
    /**
     * @var array a list of {@link CCodeFile} objects that represent the code files to be generated.
     * The {@link prepare()} method is responsible to populate this property.
     */
    public $files = array ();
    /**
     * @var integer the status of this model. T
     * The value of this property is internally managed by {@link CCodeGenerator}.
     */
    public $status = self::STATUS_NEW;
    
    private $_stickyAttributes = array ();
    
    /**
     * Prepares the code files to be generated.
     * This is the main method that child classes should implement. It should contain the logic
     * that populates the {@link files} property with a list of code files to be generated.
     */
    abstract public function prepare();
    
    /**
     * Declares the model validation rules.
     * Child classes must override this method in the following format:
     * <pre>
     * return array_merge(parent::rules(), array(
     * ...rules for the child class...
     * ));
     * </pre>
     * @return array validation rules
     */
    public function rules()
    {
        return array (array ('template', 'required' ), array ('template', 'validateTemplate', 'skipOnError' => true ), array ('template', 'sticky' ) );
    }
    
    /**
     * Validates the template selection.
     * This method validates whether the user selects an existing template
     * and the template contains all required template files as specified in {@link requiredTemplates}.
     * @param string $attribute the attribute to be validated
     * @param array $params validation parameters
     */
    public function validateTemplate($attribute, $params)
    {
        $templates = $this->templates;
        if (! isset ( $templates [$this->template] ))
            $this->addError ( 'template', 'Invalid template selection.' );
        else {
            $templatePath = $this->templatePath;
            foreach ( $this->requiredTemplates () as $template ) {
                if (! is_file ( $templatePath . '/' . $template ))
                    $this->addError ( 'template', "Unable to find the required code template file '$template'." );
            }
        }
    }
    
    /**
     * Checks if the named class exists (in a case sensitive manner).
     * @param string $name class name to be checked
     * @return boolean whether the class exists
     */
    public function classExists($name)
    {
        return class_exists ( $name, false ) && in_array ( $name, get_declared_classes () );
    }
    
    /**
     * Declares the model attribute labels.
     * Child classes must override this method in the following format:
     * <pre>
     * return array_merge(parent::attributeLabels(), array(
     * ...labels for the child class attributes...
     * ));
     * </pre>
     * @return array the attribute labels
     */
    public function attributeLabels()
    {
        return array ('template' => 'Code Template' );
    }
    
    /**
     * Returns a list of code templates that are required.
     * Derived classes usually should override this method.
     * @return array list of code templates that are required. They should be file paths
     * relative to {@link templatePath}.
     */
    public function requiredTemplates()
    {
        return array ();
    }
    
    /**
     * Saves the generated code into files.
     */
    public function save()
    {
        $result = true;
        foreach ( $this->files as $file ) {
            if ($this->confirmed ( $file ))
                $result = $file->save () && $result;
        }
        return $result;
    }
    
    /**
     * Returns the message to be displayed when the newly generated code is saved successfully.
     * Child classes should override this method if the message needs to be customized.
     * @return string the message to be displayed when the newly generated code is saved successfully.
     */
    public function successMessage()
    {
        return 'The code has been generated successfully.';
    }
    
    /**
     * Returns the message to be displayed when some error occurred during code file saving.
     * Child classes should override this method if the message needs to be customized.
     * @return string the message to be displayed when some error occurred during code file saving.
     */
    public function errorMessage()
    {
        return 'There was some error when generating the code. Please check the following messages.';
    }
    
    /**
     * Returns a list of available code templates (name=>directory).
     * This method simply returns the {@link CCodeGenerator::templates} property value.
     * @return array a list of available code templates (name=>directory).
     */
    public function getTemplates()
    {
        return Yii::app ()->controller->templates;
    }
    
    /**
     * @return string the directory that contains the template files.
     * @throw CException if {@link templates} is empty or template selection is invalid
     */
    public function getTemplatePath()
    {
        $templates = $this->getTemplates ();
        if (isset ( $templates [$this->template] ))
            return $templates [$this->template];
        else if (empty ( $templates ))
            throw new CHttpException ( 500, 'No templates are available.' );
        else
            throw new CHttpException ( 500, 'Invalid template selection.' );
    
    }
    
    /**
     * @param CCodeFile $file whether the code file should be saved
     */
    public function confirmed($file)
    {
        return $this->answers === null && $file->operation === CCodeFile::OP_NEW || is_array ( $this->answers ) && isset ( $this->answers [md5 ( $file->path )] );
    }
    
    /**
     * Generates the code using the specified code template file.
     * This method is manly used in {@link generate} to generate code.
     * @param string $templateFile the code template file path
     * @param array $_params_ a set of parameters to be extracted and made available in the code template
     * @return string the generated code
     */
    public function render($templateFile, $_params_ = null)
    {
        if (! is_file ( $templateFile ))
            throw new CException ( "The template file '$templateFile' does not exist." );
        
        if (is_array ( $_params_ ))
            extract ( $_params_, EXTR_PREFIX_SAME, 'params' );
        else
            $params = $_params_;
        ob_start ();
        ob_implicit_flush ( false );
        require ($templateFile);
        return ob_get_clean ();
    }
    
    /**
     * @return string the code generation result log.
     */
    public function renderResults()
    {
        $output = 'Generating code using template "' . $this->templatePath . "\"...\n";
        foreach ( $this->files as $file ) {
            if ($file->error !== null)
                $output .= "<span class=\"error\">generating {$file->relativePath}<br/>           {$file->error}</span>\n";
            else if ($file->operation === CCodeFile::OP_NEW && $this->confirmed ( $file ))
                $output .= ' generated ' . $file->relativePath . "\n";
            else if ($file->operation === CCodeFile::OP_OVERWRITE && $this->confirmed ( $file ))
                $output .= ' overwrote ' . $file->relativePath . "\n";
            else
                $output .= '   skipped ' . $file->relativePath . "\n";
        }
        $output .= "done!\n";
        return $output;
    }
    
    /**
     * The "sticky" validator.
     * This validator does not really validate the attributes.
     * It actually saves the attribute value in a file to make it sticky.
     * @param string $attribute the attribute to be validated
     * @param array $params the validation parameters
     */
    public function sticky($attribute, $params)
    {
        if (! $this->hasErrors ())
            $this->_stickyAttributes [$attribute] = $this->$attribute;
    }
    
    /**
     * Loads sticky attributes from a file and populates them into the model.
     */
    public function loadStickyAttributes()
    {
        $this->_stickyAttributes = array ();
        $path = $this->getStickyFile ();
        if (is_file ( $path )) {
            $result = @include ($path);
            if (is_array ( $result )) {
                $this->_stickyAttributes = $result;
                foreach ( $this->_stickyAttributes as $name => $value ) {
                    if (property_exists ( $this, $name ) || $this->canSetProperty ( $name ))
                        $this->$name = $value;
                }
            }
        }
    }
    
    /**
     * Saves sticky attributes into a file.
     */
    public function saveStickyAttributes()
    {
        $path = $this->getStickyFile ();
        @mkdir ( dirname ( $path ), 0755, true );
        file_put_contents ( $path, "<?php\nreturn " . var_export ( $this->_stickyAttributes, true ) . ";\n" );
    }
    
    /**
     * @return string the file path that stores the sticky attribute values.
     */
    public function getStickyFile()
    {
        return Yii::app ()->runtimePath . '/gii-' . Yii::getVersion () . '/' . get_class ( $this ) . '.php';
    }
    
    /**
     * Converts a word to its plural form.
     * Note that this is for English only!
     * For example, 'apple' will become 'apples', and 'child' will become 'children'.
     * @param string $name the word to be pluralized
     * @return string the pluralized word
     */
    public function pluralize($name)
    {
        $rules = array ('/(x|ch|ss|sh|us|as|is|os)$/i' => '\1es', '/(?:([^f])fe|([lr])f)$/i' => '\1\2ves', '/(m)an$/i' => '\1en', '/(child)$/i' => '\1ren', '/(r|t)y$/i' => '\1ies', '/s$/' => 's' );
        foreach ( $rules as $rule => $replacement ) {
            if (preg_match ( $rule, $name ))
                return preg_replace ( $rule, $replacement, $name );
        }
        return $name . 's';
    }
    
    /**
     * Converts a class name into a HTML ID.
     * For example, 'PostTag' will be converted as 'post-tag'.
     * @param string $name the string to be converted
     * @return string the resulting ID
     */
    public function class2id($name)
    {
        return trim ( strtolower ( str_replace ( '_', '-', preg_replace ( '/(?<![A-Z])[A-Z]/', '-\0', $name ) ) ), '-' );
    }
    
    /**
     * Converts a class name into space-separated words.
     * For example, 'PostTag' will be converted as 'Post Tag'.
     * @param string $name the string to be converted
     * @param boolean $ucwords whether to capitalize the first letter in each word
     * @return string the resulting words
     */
    public function class2name($name, $ucwords = true)
    {
        $result = trim ( strtolower ( str_replace ( '_', ' ', preg_replace ( '/(?<![A-Z])[A-Z]/', ' \0', $name ) ) ) );
        return $ucwords ? ucwords ( $result ) : $result;
    }
    
    /**
     * Converts a class name into a variable name with the first letter in lower case.
     * This method is provided because lcfirst() PHP function is only available for PHP 5.3+.
     * @param string $name the class name
     * @return string the variable name converted from the class name
     * @since 1.1.4
     */
    public function class2var($name)
    {
        $name [0] = strtolower ( $name [0] );
        return $name;
    }
    
    /**
     * Validates an attribute to make sure it is not taking a PHP reserved keyword.
     * @param string $attribute the attribute to be validated
     * @param array $params validation parameters
     */
    public function validateReservedWord($attribute, $params)
    {
        static $keywords = array ('__class__', '__dir__', '__file__', '__function__', '__line__', '__method__', '__namespace__', 'abstract', 'and', 'array', 'as', 'break', 'case', 'catch', 'cfunction', 'class', 'clone', 'const', 'continue', 'declare', 'default', 'die', 'do', 'echo', 'else', 'elseif', 'empty', 'enddeclare', 'endfor', 'endforeach', 'endif', 'endswitch', 'endwhile', 'eval', 'exception', 'exit', 'extends', 'final', 'final', 'for', 'foreach', 'function', 'global', 'goto', 'if', 'implements', 'include', 'include_once', 'instanceof', 'interface', 'isset', 'list', 'namespace', 'new', 'old_function', 'or', 'parent', 'php_user_filter', 'print', 'private', 'protected', 'public', 'require', 'require_once', 'return', 'static', 'switch', 'this', 'throw', 'try', 'unset', 'use', 'var', 'while', 'xor' );
        $value = $this->$attribute;
        if (in_array ( strtolower ( $value ), $keywords ))
            $this->addError ( $attribute, $this->getAttributeLabel ( $attribute ) . ' cannot take a reserved PHP keyword.' );
    }
}